| Conditions | 59 |
| Total Lines | 356 |
| Code Lines | 228 |
| Lines | 0 |
| Ratio | 0 % |
| Changes | 0 | ||
Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.
For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.
Commonly applied refactorings include:
If many parameters/temporary variables are present:
Complex classes like text_layer_builder.js ➔ TextLayerBuilderClosure often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
||
| 43 | var TextLayerBuilder = (function TextLayerBuilderClosure() { |
||
| 44 | function TextLayerBuilder(options) { |
||
| 45 | this.textLayerDiv = options.textLayerDiv; |
||
| 46 | this.renderingDone = false; |
||
| 47 | this.divContentDone = false; |
||
| 48 | this.pageIdx = options.pageIndex; |
||
| 49 | this.pageNumber = this.pageIdx + 1; |
||
| 50 | this.matches = []; |
||
| 51 | this.viewport = options.viewport; |
||
| 52 | this.textDivs = []; |
||
| 53 | this.findController = options.findController || null; |
||
| 54 | } |
||
| 55 | |||
| 56 | TextLayerBuilder.prototype = { |
||
| 57 | _finishRendering: function TextLayerBuilder_finishRendering() { |
||
| 58 | this.renderingDone = true; |
||
| 59 | |||
| 60 | var event = document.createEvent('CustomEvent'); |
||
| 61 | event.initCustomEvent('textlayerrendered', true, true, { |
||
| 62 | pageNumber: this.pageNumber |
||
| 63 | }); |
||
| 64 | this.textLayerDiv.dispatchEvent(event); |
||
| 65 | }, |
||
| 66 | |||
| 67 | renderLayer: function TextLayerBuilder_renderLayer() { |
||
| 68 | var textLayerFrag = document.createDocumentFragment(); |
||
| 69 | var textDivs = this.textDivs; |
||
| 70 | var textDivsLength = textDivs.length; |
||
| 71 | var canvas = document.createElement('canvas'); |
||
| 72 | var ctx = canvas.getContext('2d'); |
||
| 73 | |||
| 74 | // No point in rendering many divs as it would make the browser |
||
| 75 | // unusable even after the divs are rendered. |
||
| 76 | if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) { |
||
| 77 | this._finishRendering(); |
||
| 78 | return; |
||
| 79 | } |
||
| 80 | |||
| 81 | var lastFontSize; |
||
| 82 | var lastFontFamily; |
||
| 83 | for (var i = 0; i < textDivsLength; i++) { |
||
| 84 | var textDiv = textDivs[i]; |
||
| 85 | if (textDiv.dataset.isWhitespace !== undefined) { |
||
| 86 | continue; |
||
| 87 | } |
||
| 88 | |||
| 89 | var fontSize = textDiv.style.fontSize; |
||
| 90 | var fontFamily = textDiv.style.fontFamily; |
||
| 91 | |||
| 92 | // Only build font string and set to context if different from last. |
||
| 93 | if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) { |
||
| 94 | ctx.font = fontSize + ' ' + fontFamily; |
||
| 95 | lastFontSize = fontSize; |
||
| 96 | lastFontFamily = fontFamily; |
||
| 97 | } |
||
| 98 | |||
| 99 | var width = ctx.measureText(textDiv.textContent).width; |
||
| 100 | if (width > 0) { |
||
| 101 | textLayerFrag.appendChild(textDiv); |
||
| 102 | var transform; |
||
| 103 | if (textDiv.dataset.canvasWidth !== undefined) { |
||
| 104 | // Dataset values come of type string. |
||
| 105 | var textScale = textDiv.dataset.canvasWidth / width; |
||
| 106 | transform = 'scaleX(' + textScale + ')'; |
||
| 107 | } else { |
||
| 108 | transform = ''; |
||
| 109 | } |
||
| 110 | var rotation = textDiv.dataset.angle; |
||
| 111 | if (rotation) { |
||
| 112 | transform = 'rotate(' + rotation + 'deg) ' + transform; |
||
| 113 | } |
||
| 114 | if (transform) { |
||
| 115 | CustomStyle.setProp('transform' , textDiv, transform); |
||
| 116 | } |
||
| 117 | } |
||
| 118 | } |
||
| 119 | |||
| 120 | this.textLayerDiv.appendChild(textLayerFrag); |
||
| 121 | this._finishRendering(); |
||
| 122 | this.updateMatches(); |
||
| 123 | }, |
||
| 124 | |||
| 125 | /** |
||
| 126 | * Renders the text layer. |
||
| 127 | * @param {number} timeout (optional) if specified, the rendering waits |
||
| 128 | * for specified amount of ms. |
||
| 129 | */ |
||
| 130 | render: function TextLayerBuilder_render(timeout) { |
||
| 131 | if (!this.divContentDone || this.renderingDone) { |
||
| 132 | return; |
||
| 133 | } |
||
| 134 | |||
| 135 | if (this.renderTimer) { |
||
| 136 | clearTimeout(this.renderTimer); |
||
| 137 | this.renderTimer = null; |
||
| 138 | } |
||
| 139 | |||
| 140 | if (!timeout) { // Render right away |
||
| 141 | this.renderLayer(); |
||
| 142 | } else { // Schedule |
||
| 143 | var self = this; |
||
| 144 | this.renderTimer = setTimeout(function() { |
||
| 145 | self.renderLayer(); |
||
| 146 | self.renderTimer = null; |
||
| 147 | }, timeout); |
||
| 148 | } |
||
| 149 | }, |
||
| 150 | |||
| 151 | appendText: function TextLayerBuilder_appendText(geom, styles) { |
||
| 152 | var style = styles[geom.fontName]; |
||
| 153 | var textDiv = document.createElement('div'); |
||
| 154 | this.textDivs.push(textDiv); |
||
| 155 | if (isAllWhitespace(geom.str)) { |
||
| 156 | textDiv.dataset.isWhitespace = true; |
||
| 157 | return; |
||
| 158 | } |
||
| 159 | var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); |
||
| 160 | var angle = Math.atan2(tx[1], tx[0]); |
||
| 161 | if (style.vertical) { |
||
| 162 | angle += Math.PI / 2; |
||
| 163 | } |
||
| 164 | var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); |
||
| 165 | var fontAscent = fontHeight; |
||
| 166 | if (style.ascent) { |
||
| 167 | fontAscent = style.ascent * fontAscent; |
||
| 168 | } else if (style.descent) { |
||
| 169 | fontAscent = (1 + style.descent) * fontAscent; |
||
| 170 | } |
||
| 171 | |||
| 172 | var left; |
||
| 173 | var top; |
||
| 174 | if (angle === 0) { |
||
| 175 | left = tx[4]; |
||
| 176 | top = tx[5] - fontAscent; |
||
| 177 | } else { |
||
| 178 | left = tx[4] + (fontAscent * Math.sin(angle)); |
||
| 179 | top = tx[5] - (fontAscent * Math.cos(angle)); |
||
| 180 | } |
||
| 181 | textDiv.style.left = left + 'px'; |
||
| 182 | textDiv.style.top = top + 'px'; |
||
| 183 | textDiv.style.fontSize = fontHeight + 'px'; |
||
| 184 | textDiv.style.fontFamily = style.fontFamily; |
||
| 185 | |||
| 186 | textDiv.textContent = geom.str; |
||
| 187 | // |fontName| is only used by the Font Inspector. This test will succeed |
||
| 188 | // when e.g. the Font Inspector is off but the Stepper is on, but it's |
||
| 189 | // not worth the effort to do a more accurate test. |
||
| 190 | if (PDFJS.pdfBug) { |
||
| 191 | textDiv.dataset.fontName = geom.fontName; |
||
| 192 | } |
||
| 193 | // Storing into dataset will convert number into string. |
||
| 194 | if (angle !== 0) { |
||
| 195 | textDiv.dataset.angle = angle * (180 / Math.PI); |
||
| 196 | } |
||
| 197 | // We don't bother scaling single-char text divs, because it has very |
||
| 198 | // little effect on text highlighting. This makes scrolling on docs with |
||
| 199 | // lots of such divs a lot faster. |
||
| 200 | if (textDiv.textContent.length > 1) { |
||
| 201 | if (style.vertical) { |
||
| 202 | textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; |
||
| 203 | } else { |
||
| 204 | textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; |
||
| 205 | } |
||
| 206 | } |
||
| 207 | }, |
||
| 208 | |||
| 209 | setTextContent: function TextLayerBuilder_setTextContent(textContent) { |
||
| 210 | this.textContent = textContent; |
||
| 211 | |||
| 212 | var textItems = textContent.items; |
||
| 213 | for (var i = 0, len = textItems.length; i < len; i++) { |
||
| 214 | this.appendText(textItems[i], textContent.styles); |
||
| 215 | } |
||
| 216 | this.divContentDone = true; |
||
| 217 | }, |
||
| 218 | |||
| 219 | convertMatches: function TextLayerBuilder_convertMatches(matches) { |
||
| 220 | var i = 0; |
||
| 221 | var iIndex = 0; |
||
| 222 | var bidiTexts = this.textContent.items; |
||
| 223 | var end = bidiTexts.length - 1; |
||
| 224 | var queryLen = (this.findController === null ? |
||
| 225 | 0 : this.findController.state.query.length); |
||
| 226 | var ret = []; |
||
| 227 | |||
| 228 | for (var m = 0, len = matches.length; m < len; m++) { |
||
| 229 | // Calculate the start position. |
||
| 230 | var matchIdx = matches[m]; |
||
| 231 | |||
| 232 | // Loop over the divIdxs. |
||
| 233 | while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { |
||
| 234 | iIndex += bidiTexts[i].str.length; |
||
| 235 | i++; |
||
| 236 | } |
||
| 237 | |||
| 238 | if (i === bidiTexts.length) { |
||
| 239 | console.error('Could not find a matching mapping'); |
||
| 240 | } |
||
| 241 | |||
| 242 | var match = { |
||
| 243 | begin: { |
||
| 244 | divIdx: i, |
||
| 245 | offset: matchIdx - iIndex |
||
| 246 | } |
||
| 247 | }; |
||
| 248 | |||
| 249 | // Calculate the end position. |
||
| 250 | matchIdx += queryLen; |
||
| 251 | |||
| 252 | // Somewhat the same array as above, but use > instead of >= to get |
||
| 253 | // the end position right. |
||
| 254 | while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { |
||
| 255 | iIndex += bidiTexts[i].str.length; |
||
| 256 | i++; |
||
| 257 | } |
||
| 258 | |||
| 259 | match.end = { |
||
| 260 | divIdx: i, |
||
| 261 | offset: matchIdx - iIndex |
||
| 262 | }; |
||
| 263 | ret.push(match); |
||
| 264 | } |
||
| 265 | |||
| 266 | return ret; |
||
| 267 | }, |
||
| 268 | |||
| 269 | renderMatches: function TextLayerBuilder_renderMatches(matches) { |
||
| 270 | // Early exit if there is nothing to render. |
||
| 271 | if (matches.length === 0) { |
||
| 272 | return; |
||
| 273 | } |
||
| 274 | |||
| 275 | var bidiTexts = this.textContent.items; |
||
| 276 | var textDivs = this.textDivs; |
||
| 277 | var prevEnd = null; |
||
| 278 | var pageIdx = this.pageIdx; |
||
| 279 | var isSelectedPage = (this.findController === null ? |
||
| 280 | false : (pageIdx === this.findController.selected.pageIdx)); |
||
| 281 | var selectedMatchIdx = (this.findController === null ? |
||
| 282 | -1 : this.findController.selected.matchIdx); |
||
| 283 | var highlightAll = (this.findController === null ? |
||
| 284 | false : this.findController.state.highlightAll); |
||
| 285 | var infinity = { |
||
| 286 | divIdx: -1, |
||
| 287 | offset: undefined |
||
| 288 | }; |
||
| 289 | |||
| 290 | function beginText(begin, className) { |
||
| 291 | var divIdx = begin.divIdx; |
||
| 292 | textDivs[divIdx].textContent = ''; |
||
| 293 | appendTextToDiv(divIdx, 0, begin.offset, className); |
||
| 294 | } |
||
| 295 | |||
| 296 | function appendTextToDiv(divIdx, fromOffset, toOffset, className) { |
||
| 297 | var div = textDivs[divIdx]; |
||
| 298 | var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); |
||
| 299 | var node = document.createTextNode(content); |
||
| 300 | if (className) { |
||
| 301 | var span = document.createElement('span'); |
||
| 302 | span.className = className; |
||
| 303 | span.appendChild(node); |
||
| 304 | div.appendChild(span); |
||
| 305 | return; |
||
| 306 | } |
||
| 307 | div.appendChild(node); |
||
| 308 | } |
||
| 309 | |||
| 310 | var i0 = selectedMatchIdx, i1 = i0 + 1; |
||
| 311 | if (highlightAll) { |
||
| 312 | i0 = 0; |
||
| 313 | i1 = matches.length; |
||
| 314 | } else if (!isSelectedPage) { |
||
| 315 | // Not highlighting all and this isn't the selected page, so do nothing. |
||
| 316 | return; |
||
| 317 | } |
||
| 318 | |||
| 319 | for (var i = i0; i < i1; i++) { |
||
| 320 | var match = matches[i]; |
||
| 321 | var begin = match.begin; |
||
| 322 | var end = match.end; |
||
| 323 | var isSelected = (isSelectedPage && i === selectedMatchIdx); |
||
| 324 | var highlightSuffix = (isSelected ? ' selected' : ''); |
||
| 325 | |||
| 326 | if (this.findController) { |
||
| 327 | this.findController.updateMatchPosition(pageIdx, i, textDivs, |
||
| 328 | begin.divIdx, end.divIdx); |
||
| 329 | } |
||
| 330 | |||
| 331 | // Match inside new div. |
||
| 332 | if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { |
||
| 333 | // If there was a previous div, then add the text at the end. |
||
| 334 | if (prevEnd !== null) { |
||
| 335 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); |
||
| 336 | } |
||
| 337 | // Clear the divs and set the content until the starting point. |
||
| 338 | beginText(begin); |
||
| 339 | } else { |
||
| 340 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); |
||
| 341 | } |
||
| 342 | |||
| 343 | if (begin.divIdx === end.divIdx) { |
||
| 344 | appendTextToDiv(begin.divIdx, begin.offset, end.offset, |
||
| 345 | 'highlight' + highlightSuffix); |
||
| 346 | } else { |
||
| 347 | appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, |
||
| 348 | 'highlight begin' + highlightSuffix); |
||
| 349 | for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { |
||
| 350 | textDivs[n0].className = 'highlight middle' + highlightSuffix; |
||
| 351 | } |
||
| 352 | beginText(end, 'highlight end' + highlightSuffix); |
||
| 353 | } |
||
| 354 | prevEnd = end; |
||
| 355 | } |
||
| 356 | |||
| 357 | if (prevEnd) { |
||
| 358 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); |
||
| 359 | } |
||
| 360 | }, |
||
| 361 | |||
| 362 | updateMatches: function TextLayerBuilder_updateMatches() { |
||
| 363 | // Only show matches when all rendering is done. |
||
| 364 | if (!this.renderingDone) { |
||
| 365 | return; |
||
| 366 | } |
||
| 367 | |||
| 368 | // Clear all matches. |
||
| 369 | var matches = this.matches; |
||
| 370 | var textDivs = this.textDivs; |
||
| 371 | var bidiTexts = this.textContent.items; |
||
| 372 | var clearedUntilDivIdx = -1; |
||
| 373 | |||
| 374 | // Clear all current matches. |
||
| 375 | for (var i = 0, len = matches.length; i < len; i++) { |
||
| 376 | var match = matches[i]; |
||
| 377 | var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); |
||
| 378 | for (var n = begin, end = match.end.divIdx; n <= end; n++) { |
||
| 379 | var div = textDivs[n]; |
||
| 380 | div.textContent = bidiTexts[n].str; |
||
| 381 | div.className = ''; |
||
| 382 | } |
||
| 383 | clearedUntilDivIdx = match.end.divIdx + 1; |
||
| 384 | } |
||
| 385 | |||
| 386 | if (this.findController === null || !this.findController.active) { |
||
| 387 | return; |
||
| 388 | } |
||
| 389 | |||
| 390 | // Convert the matches on the page controller into the match format |
||
| 391 | // used for the textLayer. |
||
| 392 | this.matches = this.convertMatches(this.findController === null ? |
||
| 393 | [] : (this.findController.pageMatches[this.pageIdx] || [])); |
||
| 394 | this.renderMatches(this.matches); |
||
| 395 | } |
||
| 396 | }; |
||
| 397 | return TextLayerBuilder; |
||
| 398 | })(); |
||
| 399 | |||
| 420 |